/*
 * Copyright (C) 2010 http://laantiguafragua.blogspot.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * SimpleFileUpdate.java
 *
 * Created on 02/24/2010
 */
package simplefileupdate;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

/**
 * A simple java library used for updating and maintaining files of a project.
 * @author lhrod
 */
public class SimpleFileUpdate implements Runnable {

    private VersionData verDataSource = null;
    private VersionData verDataDestiny = null;
    private String defaultDir = "";
    public static final int DOWNLOAD = 0;
    public static final int RENAME = 1;
    public static final int DELETE = 2;
    public static final int NEWS = 3;
    public static final int STOPPED = Integer.MAX_VALUE;
    private boolean stop = true;
    private long copyCount = 0;
    private long downSize = 0;
    private Hashtable actionHash = new Hashtable();
    private int task = STOPPED;
    private boolean reloadRec = false;

    public boolean isRun() {
        return !stop;
    }

    public void setVerDataSource(VersionData verDataSource) {
        this.verDataSource = verDataSource;
    }

    public void setVerDataDestiny(VersionData verDataDestiny) {
        this.verDataDestiny = verDataDestiny;
    }

    public Hashtable getActionHash() {
        return actionHash;
    }

    /**
     * This method calculates a changes to update the files.
     * @return A Hashtable contains three entries:
     *             <br/>DELETE: The files in this entries will be deleted.
     *             <br/>RENAME: The files in this entries will be renamed.
     *             <br/>DOWNLOAD: The files in this entries will be downloaded and overwrited.
     */
    public void calculateActionHash() throws NullPointerException {
        ArrayList copyArray = new ArrayList();
        ArrayList renArray = new ArrayList();
        ArrayList delArray = new ArrayList();

        if (verDataSource == null) {
            throw new NullPointerException("verDataSource is null");
        }
        if (verDataDestiny == null) {
            throw new NullPointerException("verDataDestiny is null");
        }

        Hashtable fileA = verDataSource.getFileHash();
        Hashtable fileB = verDataDestiny.getFileHash();

        for (FileData fA : (FileData[]) fileA.values().toArray(new FileData[]{})) {
            // If fA exist in the source and not exists in the destiny, it must be copied.
            if (!fileB.containsKey(fA.id)) {
                copyArray.add(fA);
                downSize += fA.size;
            } else {
                FileData fB = (FileData) fileB.get(fA.id);
                // The versions are equal
                if (fB.version.equals(fA.version)) {
                    // The filenames are different
                    if (!fB.name.equals(fA.name)) {
                        renArray.add(fA);
                    }
                } else // The versions aren't equal
                {
                    float verA = Float.parseFloat(fA.version);
                    float verB = Float.parseFloat(fB.version);
                    // The new version in the source, the new version are copy
                    if (verA > verB) {
                        copyArray.add(fA);
                        downSize += fA.size;
                    }
                }
            }
        }

        for (FileData fB : (FileData[]) fileB.values().toArray(new FileData[]{})) {
            File f = new File(this.defaultDir + fB.name);
            // If fA exist in the destiny and not exists in the source, it must be deleted.
            if (!fileA.containsKey(fB.id)) {
                if (f.exists()) {
                    delArray.add(fB);
                }
            } // if file contain in destiny, source and file not exist in destiny, this file have download
            else if (!f.exists()) {
                FileData fA = (FileData) fileA.get(fB.id);
                if (!copyArray.contains(fA)) {
                    copyArray.add(fA);
                    downSize += fA.size;
                }
            }
        }

        actionHash.clear();
        actionHash.put(DOWNLOAD, copyArray.toArray(new FileData[0]));
        actionHash.put(RENAME, renArray.toArray(new FileData[0]));
        actionHash.put(DELETE, delArray.toArray(new FileData[0]));
        actionHash.put(NEWS, verDataSource.getNews());
    }

    public void run() {
        stop = false;
        if (actionHash.isEmpty()) {
            stop = true;
            throw new NullPointerException("Action isn't calculate");
        }

        ArrayList renShutArr = new ArrayList();
        ArrayList delShutArr = new ArrayList();

        reloadRec = false;
        task = DELETE;
        for (FileData file : (FileData[]) actionHash.get(task)) {
            File f = new File(defaultDir + file.name);
            if (f.exists()) {
                if (!file.reloadApp) {
                    try {
                        if (!f.delete()) {
                            stop = true;
                            throw new IOException("File \"" + f + "\" can't delete...");
                        }
                    } catch (IOException ex) {
                        Logger.getLogger(SimpleFileUpdate.class.getName()).log(Level.SEVERE, null, ex);
                        ex.printStackTrace();
                    }
                } else {
                    delShutArr.add(f);
                }
            } else {
                Logger.getLogger(LoadXML.class.getName()).log(Level.SEVERE, null, new FileNotFoundException("File " + f + " not exist"));
            }
            reloadRec |= file.reloadApp;
        }

        task = RENAME;
        for (FileData file : (FileData[]) actionHash.get(task)) {
            File f = new File(defaultDir + file.name);
            FileData fB = (FileData) this.verDataDestiny.getFileHash().get(file.id);
            File fTmp = new File(defaultDir + fB.name);
            if (!file.reloadApp) {
                try {
                    if (!fTmp.renameTo(f)) {
                        stop = true;
                        throw new IOException("File \"" + fTmp + "\" can't rename to \"" + f + "\"");
                    }
                } catch (IOException ex) {
                    Logger.getLogger(SimpleFileUpdate.class.getName()).log(Level.SEVERE, null, ex);
                    ex.printStackTrace();
                }
            } else {
                renShutArr.add(new File[]{fTmp, f});
            }
            reloadRec |= file.reloadApp;
        }

        task = DOWNLOAD;
        for (FileData file : (FileData[]) actionHash.get(task)) {
            File f = new File(defaultDir + file.name);
            URL url = file.url;
            File fTmp = new File(defaultDir + System.currentTimeMillis() + ".tmp");
            try {
                createCopy(url, fTmp);
                if (!file.reloadApp) {
                    if (f.exists() && !f.delete()) {
                        stop = true;
                        throw new IOException("Old file \"" + f + "\" can't delete...");
                    }
                    if (!fTmp.renameTo(new File(defaultDir + file.name))) {
                        stop = true;
                        throw new IOException("File \"" + fTmp + "\" can't rename to \"" + f + "\"");
                    }
                } else {
                    delShutArr.add(f);
                    renShutArr.add(new File[]{fTmp, f});
                }
            } catch (Exception e) {
                Logger.getLogger(LoadXML.class.getName()).log(Level.SEVERE, null, e);
                e.printStackTrace();
            }
            reloadRec |= file.reloadApp;
        }

        if (reloadRec) {
            Thread t = new Thread(new ShutDownAction(renShutArr, delShutArr));
            Runtime.getRuntime().addShutdownHook(t);
        }

        stop = true;
    }

    /**
     * Size of bytes downloaded
     * @return
     */
    public long getDownCount() {
        return copyCount;
    }

    /**
     * @return the defaultDir
     */
    public String getDefaultDir() {
        return defaultDir;
    }

    /**
     * @param defaultDir the defaultDir to set
     */
    public void setDefaultDir(String defaultDir) {
        if (!defaultDir.endsWith(File.separator)) {
            defaultDir += File.separator;
        }
        this.defaultDir = defaultDir;
    }

    /**
     *
     * @param actionHash
     * @return
     */
    @Override
    public String toString() {
        String res = "";


        String[] newsA = (String[]) actionHash.get(SimpleFileUpdate.NEWS);
        int iNews = newsA.length;
        if (iNews > 0) {
            res = " News:\n";
            for (String n : newsA) {
                res += "  -" + n + "\n";
            }
            res += "\n";
        }

        if (!this.isActionPending()) {
            return res + "No updates pending...";
        }

        res += toString((FileData[]) actionHash.get(SimpleFileUpdate.DOWNLOAD), "Download");
        res += toString((FileData[]) actionHash.get(SimpleFileUpdate.RENAME), "Rename");
        res += toString((FileData[]) actionHash.get(SimpleFileUpdate.DELETE), "Delete");

        return res;
    }

    private String toString(FileData[] fA, String nameAction) {
        String res = "";
        String[] newsA;
        int iNews;
        if (fA.length > 0) {

            res += nameAction + ":\n";
            for (int i = 0; i < fA.length; i++) {
                newsA = fA[i].getNews();
                iNews = newsA.length;
                res += "  * " + fA[i].name + ((iNews > 0) ? ":\n      News:\n" : "\n");
                for (String n : newsA) {
                    res += "          -" + n + "\n";
                }
            }
        }
        return res;
    }

    /**
     * Get total size to download
     * @return
     */
    public long getDownSize() {
        return downSize;
    }

    public int getTask() {
        return task;
    }

    public boolean isReloadRecommended() {
        return reloadRec;
    }

    public boolean isActionPending() {
        boolean ret = (((FileData[]) actionHash.get(SimpleFileUpdate.DELETE)).length > 0);
        ret = ret || (((FileData[]) actionHash.get(SimpleFileUpdate.DOWNLOAD)).length > 0);
        ret = ret || (((FileData[]) actionHash.get(SimpleFileUpdate.RENAME)).length > 0);
        return ret;
    }

    private static byte[] buffer = new byte[1500];

    private void createCopy(URL url, File f) throws FileNotFoundException, IOException {
        FileOutputStream fOut = new FileOutputStream(f);
        URLConnection urlC = url.openConnection();
        InputStream uIn = urlC.getInputStream();

        int c;
        while ((c = uIn.read(buffer)) != -1) {
            fOut.write(buffer, 0, c);
            copyCount += c;
        }
        fOut.close();
        uIn.close();
    }

    public static void createCopy(InputStream uIn, File f) throws FileNotFoundException, IOException {
        FileOutputStream fOut = new FileOutputStream(f);

        int c;
        while ((c = uIn.read(buffer)) != -1) {
            fOut.write(buffer, 0, c);
        }
        fOut.close();
    }
}

/**
 * This class finishes the updates after having finished the program.
 * @author Lhrod
 */
class ShutDownAction implements Runnable {
    private ArrayList renArr = null;
    private ArrayList delArr = null;

    public ShutDownAction(ArrayList renArr, ArrayList delArr) {
        this.renArr = renArr;
        this.delArr = delArr;
    }
    static final String SEP1 = "\n";
    static final String SEP2 = "\t";
    static final String SFUAgentName = "SFUShutdownAgent";
    static final String SFUAgentFileClass = SFUAgentName + ".class";

    public void run() {
        String pathTmp = System.getProperty("java.io.tmpdir");
        if (!pathTmp.endsWith(File.separator)) {
            pathTmp += File.separator;
        }

        URL urlPackageClass = getClass().getResource(this.getClass().getSimpleName() + ".class");
        File SFUFile = new File(urlPackageClass.getFile()).getParentFile().getParentFile();
        InputStream uIn = null;
        String SFUPath = SFUFile.getPath();
        if (urlPackageClass.getProtocol().toLowerCase().equals("jar")) {
            SFUPath = SFUPath.replaceFirst("file:", "");
            SFUPath = SFUPath.substring(0, SFUPath.length() - 1);

            try {
                JarFile jFile = new JarFile(SFUPath);
                ZipEntry zEntry = jFile.getEntry(SFUAgentFileClass);
                uIn = jFile.getInputStream(zEntry);
            } catch (IOException ex) {
                Logger.getLogger(ShutDownAction.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else {
            SFUPath += (SFUPath.endsWith(File.separator)) ? SFUAgentFileClass : File.separator + SFUAgentFileClass;
            try {
                URL urlSFUAgentClass = new File(SFUPath).toURI().toURL();
                URLConnection urlC = urlSFUAgentClass.openConnection();
                uIn = urlC.getInputStream();
            } catch (MalformedURLException ex) {
                Logger.getLogger(ShutDownAction.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(ShutDownAction.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        File sfuTemp = null;
        try {
            sfuTemp = new File(pathTmp + SFUAgentFileClass);
            if (!sfuTemp.exists() || sfuTemp.delete()) {
                SimpleFileUpdate.createCopy(uIn, sfuTemp);
                uIn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        String delStrings = "";
        String renStrings = "";
        String sep = "";
        if (!delArr.isEmpty()) {
            for (File f : (File[]) delArr.toArray(new File[]{})) {
                delStrings += sep + f.getAbsolutePath();
                sep = SEP1;
            }
        }
        sep = "";
        if (!renArr.isEmpty()) {
            for (File[] arrObj : (File[][]) renArr.toArray(new File[][]{})) {
                renStrings += sep + arrObj[0].getAbsolutePath() + SEP2 + arrObj[1].getAbsolutePath();
                sep = SEP1;
            }
        }
        try {
            Runtime.getRuntime().exec("java -cp " + sfuTemp.getParentFile().getAbsolutePath() + " " + SFUAgentName,
                    new String[]{"deletefiles=" + delStrings, "renamefiles=" + renStrings});
        } catch (IOException ex) {
            Logger.getLogger(ShutDownAction.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

